<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/8.2.0/ol.min.css"
    integrity="sha512-bc9nJM5uKHN+wK7rtqMnzlGicwJBWR11SIDFJlYBe5fVOwjHGtXX8KMyYZ4sMgSL0CoUjo4GYgIBucOtqX/RUQ=="
    crossorigin="anonymous" referrerpolicy="no-referrer" />
  <title>图形绘制</title>

  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html,
    body,
    #app,
    .app-map {
      height: 100%;
      height: 100%;
    }

    .app-btns {
      position: fixed;
      right: 10px;
      top: 10px;
      background-color: #fff;
      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .5);
      width: 210px;
      padding: 25px;
      text-align: center;
      border-radius: 5px;
      display: flex;
      flex-direction: column;
      z-index: 2;
    }

    .app-btns button {
      font-size: 18px;
      border: none;
      padding: 12px 20px;
      border-radius: 4px;
      color: #fff;
      background-color: #409eff;
      border-color: #409eff;
      cursor: pointer;
      border: 1px solid #dcdfe6;
      margin-bottom: 5px;
    }

    .app-btns button:hover {
      background: #66b1ff;
      border-color: #66b1ff;
    }

    .app-btns button.active {
      background-color: #07c160;
    }
  </style>

</head>

<body>
  <div id="app">
    <div class="app-map" id="app-map"></div>
    <div class="app-btns">
      <button type='button' :class="{active: item.id === currentId}" v-for='item in drawTypes'
        @click='handleClickDraw(item)'>{{item.text}}</button>
      <button type='button' @click='handleClickCancel' style="background-color: #808080;">退出绘制</button>
      <button type='button' @click='handleClickDelete' style="background-color: #ff0000;">删除图形</button>
    </div>
  </div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/8.2.0/dist/ol.min.js"
    integrity="sha512-+nvfloZUX7awRy1yslYBsicmHKh/qFW5w79+AiGiNcbewg0nBy7AS4G3+aK/Rm+eGPOKlO3tLuVphMxFXeKeOQ=="
    crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.14/vue.global.prod.min.js"
    integrity="sha512-huEQFMCpBzGkSDSPVAeQFMfvWuQJWs09DslYxQ1xHeaCGQlBiky9KKZuXX7zfb0ytmgvfpTIKKAmlCZT94TAlQ=="
    crossorigin="anonymous" referrerpolicy="no-referrer"></script>

  <script>
    const { createApp } = Vue;
    // feature图片
    const base64Img = '';
    // 画时多边形鼠标悬浮样式
    const polygonHover = '';
    // 画时圆鼠标悬浮样式
    const circleHover = '';
    // 画时折线鼠标悬浮样式
    const lineStringHover = '';
    // 基础样式
    const basePointStyle = {
      src: base64Img,
      scale: 0.6,
      anchor: [0.5, 0.5],
      rotateWithView: true,
      rotation: 0,
      opacity: 1
    };
    // 根据基础样式，创建其他样式
    const createStyle = (imageParams, styleParams) => {
      return new ol.style.Style({
        image: new ol.style.Icon({
          ...basePointStyle,
          ...imageParams
        }),
        ...styleParams
      });
    };
    // 填充样式
    const fill = new ol.style.Fill({ color: 'rgba(0, 0, 0, 0.1)' });
    // 线条样式
    const stroke = new ol.style.Stroke({ color: '#07c160', width: 2 });
    // 基础样式style
    const baseStyle = createStyle({
      scale: 1
    });
    // 范围内的点位样式
    const inStyle = createStyle({ scale: 1.5 });
    // 在地图上绘制的点的样式
    const pointStyle = createStyle({ scale: 2 });
    // 圆，多边形范围外的点位的样式
    const outStyle = createStyle({ opacity: 0.6 });
    // 绘制点时鼠标悬浮的状态
    const hoverStyle = {
      "Point": createStyle({ scale: 1.5 }),
      'Polygon': createStyle(
        {
          src: polygonHover,
          scale: 0.3
        },
        {
          fill, stroke
        }
      ),
      'Circle': createStyle(
        {
          src: circleHover,
          scale: 0.3
        },
        {
          fill, stroke
        }
      ),
      'LineString': createStyle(
        {
          src: lineStringHover,
          scale: 0.3
        },
        {
          stroke
        }
      )
    };
    const vm = createApp({
      data() {
        return {
          map: {}, // 地图实例
          drawSource: {}, // 绘制图形的图层资源
          draw: null, // 绘制实例
          pointLayer: null, // 点位图层
          prevFeature: null, // 当前绘制完成的图形
          currentId: -1, // 当前绘制类型id
          closest10Features: [], // 最近的10个点位
          drawTypes: [
            {
              type: 'Circle',
              text: '画圆',
              id: 1
            },
            {
              type: 'Point',
              text: '打点',
              id: 2
            },
            {
              type: 'Polygon',
              text: '画多边形',
              id: 3
            },
            {
              type: 'LineString',
              text: '画折线',
              id: 5
            }
          ]
        }
      },
      methods: {
        // 初始化地图
        initMap() {
          // 创建放置用户绘制的feature的图层
          this.drawSource = new ol.source.Vector();
          const layer = new ol.layer.Vector({
            source: this.drawSource,
          });
          // 高德地图瓦片地址
          const mianLayer = new ol.layer.Tile({
            source: new ol.source.XYZ({
              url: 'http://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}'
            }),
            name: '初始化地图图层'
          });
          //  初始化地图
          this.map = new ol.Map({
            target: 'app-map',
            layers: [mianLayer, layer],
            view: new ol.View({
              projection: 'EPSG:3857',
              //设定中心点，因为默认坐标系为 3587，所以要将我们常用的经纬度坐标系4326 转换为 3587坐标系
              center: ol.proj.transform([111.8453154, 32.7383500], 'EPSG:4326', 'EPSG:3857'),
              zoom: 5,
            })
          });
          // 绑定地图事件
          this.bindMapEvt();
          // 渲染1000个点位到地图上
          this.renderPoint(1000);
        },
        // 绑定地图事件
        bindMapEvt() {
          // 监听鼠标点击
          this.map.on('click', (evt) => {
            const coordinate = ol.proj.transform(evt.coordinate, 'EPSG:3857', 'EPSG:4326')
            console.log('当前点击坐标为 : ' + coordinate[0].toFixed(7) + ',' + coordinate[1].toFixed(7));
          });
        },
        // 在地图上添加点位
        renderPoint(numPoints) {
          const positions = this.createCircularPosition(numPoints); // 生成坐标数据
          // 根据positions创建一个新的数据源和要素数组，
          const vectorSource = new ol.source.Vector({
            features: positions.map(e => {
              const feature = new ol.Feature({
                geometry: new ol.geom.Point(ol.proj.fromLonLat(e)),
                custom: {
                  id: Math.ceil(Math.random() * 100000)
                }
              });
              feature.setStyle(baseStyle);
              return feature;
            })
          });
          // 创建带有数据源的矢量图层
          this.pointLayer = new ol.layer.Vector({
            source: vectorSource,
            layerID: 'addpointLayer'
          });
          // 将矢量图层添加到地图上
          this.map.addLayer(this.pointLayer);
        },
        // 点击绘制各种图形
        handleClickDraw(item) {
          const { map } = this;
          const { type, geometryFunction, id } = item;
          // 同一种类型点击，不处理
          if (this.currentId === id) {
            return;
          }
          this.currentId = id;
          // 删除上一种绘制
          this.draw && map.removeInteraction(this.draw);
          // 恢复所有点位的透明度为1
          this.setFeatureOpacity();
          // 根据 type 创建交互
          this.draw = new ol.interaction.Draw({
            source: this.drawSource,
            type: type,
            style: hoverStyle[type],
            geometryFunction: geometryFunction
          });
          // 将交互添加到地图
          map.addInteraction(this.draw);
          // 监听绘制完成的事件
          this.draw.on('drawend', (event) => {
            // 获取绘制的feature
            const feature = event.feature;
            // 删除上一个feature
            this.prevFeature && this.drawSource.removeFeature(this.prevFeature);
            this.prevFeature = feature;
            // 删除折线较近的10个点位的连线
            this.removeClosestLine();
            // 如果是圆和多边形，将圆内、外的点位设置不同的样式
            if (type === "Circle" || type === 'Polygon') {
              this.handleDrawCirclePolygonEnd(feature);
            }
            // 如果是打点，计算点位距离1000个已有点位的距离，选取最近的10个，将其连接到绘制的点上
            if (type === 'Point') {
              this.handleDrawPointEnd(feature);
            }
            // 如果是折线，设置折线的颜色，计算1000个已有点位距离折线的最短距离，选取最近的10个，将其连接到折线上
            if (type === 'LineString') {
              this.handleDrawLineStringEnd(feature);
            }
          });
        },
        // 处理绘制圆和多边形的处理
        handleDrawCirclePolygonEnd(feature) {
          // 设置绘制完成后圆和多边形的样式
          feature.setStyle(new ol.style.Style({
            fill: new ol.style.Fill({ color: 'rgba(255, 0, 0, 0)' }),
            stroke: new ol.style.Stroke({ color: '#07c160', width: 2 })
          }));
          // 获取feature的Geometry
          const geometry = feature.getGeometry();
          // 遍历1000个点位
          this.pointLayer.getSource().forEachFeature(function (point) {
            const pointGeometry = point.getGeometry();
            // 判断监控点位是否在范围内
            if (geometry.intersectsExtent(pointGeometry.getExtent())) {
              point.setStyle(inStyle); // 在范围内
            } else {
              point.setStyle(outStyle); // 不在范围内
            }
          });
        },
        // 处理绘制点结束
        handleDrawPointEnd(feature) {
          // 设置点的样式
          feature.setStyle(pointStyle);
          // 获取点的投影坐标系坐标
          const pointCoord = feature.getGeometry().getCoordinates();
          // 找出和这个投影坐标系坐标最近的10个点
          const closest10 = this.calcDisFromPoint(pointCoord);
          // 10个点和绘制的点连线
          this.drawLine(closest10);
        },
        // 处理折线结束
        handleDrawLineStringEnd(feature) {
          // 设置折线样式
          feature.setStyle(new ol.style.Style({
            stroke: new ol.style.Stroke({
              color: '#07c160', // 修改边框颜色为红色
              width: 5, // 修改边框宽度
            }),
          }));
          const pointDistanceInfo = [];
          // 遍历1000个点位，计算距离
          this.pointLayer.getSource().forEachFeature(function (point) {
            // 获取折线的几何对象
            const lineGeometry = feature.getGeometry();
            // 获取点的坐标
            const pointCoords = point.getGeometry().getCoordinates();
            // 获取折线上与点最近的点
            const closestPoint = lineGeometry.getClosestPoint(pointCoords);
            // 将经纬度转换成WGS84坐标
            const pointCoordsLogLat = ol.proj.transform(pointCoords, 'EPSG:3857', 'EPSG:4326');
            const closestPointLogLat = ol.proj.transform(closestPoint, 'EPSG:3857', 'EPSG:4326');
            // 计算点位和折线的最近距离
            const currentDis = ol.sphere.getDistance(pointCoordsLogLat, closestPointLogLat);
            pointDistanceInfo.push({
              coordinate: [pointCoords, closestPoint],
              dis: currentDis,
              feature: point
            })
          });
          // 按照距离从小到大排序
          pointDistanceInfo.sort((v1, v2) => v1.dis - v2.dis);
          // 连接较近的10个点位和折线
          this.drawLine(pointDistanceInfo.slice(0, 10));
        },
        // 过滤1000个点位，计算点位和指定点的距离，返回距离最短的前10个
        calcDisFromPoint(pointWithCoords) {
          const pointDistanceInfo = [];
          // 遍历点位，计算距离
          this.pointLayer.getSource().forEachFeature(function (point) {
            // 获取点的坐标
            const pointCoords = point.getGeometry().getCoordinates();
            const pointLogLat = ol.proj.transform(pointCoords, 'EPSG:3857', 'EPSG:4326');
            const pointCoordsLonLag = ol.proj.transform(pointWithCoords, 'EPSG:3857', 'EPSG:4326');
            // 计算点位之间的距离
            const currentDis = ol.sphere.getDistance(pointLogLat, pointCoordsLonLag);
            pointDistanceInfo.push({
              coordinate: [pointWithCoords, pointCoords],
              dis: currentDis,
              feature: point
            })
          });
          // 按照距离从小到大排序
          pointDistanceInfo.sort((v1, v2) => v1.dis - v2.dis);
          return pointDistanceInfo.slice(0, 10);
        },
        // 点击取消，删除上一个交互
        handleClickCancel() {
          this.currentId = 'none';
          this.draw && this.map.removeInteraction(this.draw);
          this.draw = null;
        },
        // 删除图形
        handleClickDelete() {
          // 删除上一个feature
          this.prevFeature && this.drawSource.removeFeature(this.prevFeature);
          // 删除连线
          this.removeClosestLine();
          // 恢复点位状态
          this.setFeatureOpacity();
        },
        // 恢复点位状态
        setFeatureOpacity() {
          // 遍历1000个点位，恢复样式
          this.pointLayer.getSource().forEachFeature(function (monitorFeature) {
            monitorFeature.setStyle(baseStyle);
          });
        },
        // 两点之间绘制直线，数组中的坐标需要是投影坐标
        drawLine(pointArr) {
          for (let i = 0; i < pointArr.length; i++) {
            const { coordinate, feature } = pointArr[i];
            // 创建折线
            console.log(coordinate);
            const lineString = new ol.geom.LineString(coordinate);
            // 创建Feature
            const lineFeature = new ol.Feature(lineString);
            // 设置Feature样式
            const lineStyle = new ol.style.Style({
              stroke: new ol.style.Stroke({
                color: '#' + Math.random().toString(16).slice(2, 8),
                width: 3
              }),
            });
            // 将样式应用到要素
            lineFeature.setStyle(lineStyle);
            feature.setStyle(inStyle);
            this.closest10Features.push(lineFeature);
          }
          // 将10条线段添加到图层
          this.drawSource.addFeatures(this.closest10Features);
        },
        // 删除距离折线相对较近的点位与折线的连线
        removeClosestLine() {
          if (this.closest10Features.length > 0) {
            for (let i = 0; i < this.closest10Features.length; i++) {
              this.drawSource.removeFeature(this.closest10Features[i]);
            }
          }
          this.closest10Features = [];
        },
        // 创建数据
        createCircularPosition(numPoints) {
          const center = [108.55, 34.32];
          const circularManyPosition = [];
          const minLat = 3.86;
          const maxLat = 53.56;
          const minLon = 73.66;
          const maxLon = 135.05;
          for (var i = 0; i < numPoints; i++) {
            var lat = Math.random() * (maxLat - minLat) + minLat;
            var lon = Math.random() * (maxLon - minLon) + minLon;
            circularManyPosition.push([lon, lat]);
          }
          return circularManyPosition;
        }
      },
      mounted() {
        this.initMap();
      }
    }).mount('#app')
  </script>
</body>

</html>